﻿# nullable enable
using System.Data.Common;
using System.Linq.Expressions;
using System.Linq.Dynamic.Core;

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using System;

namespace Jack.EntityFrameworkCore
{
    public static class DbContextExtensions
    {
        public static DbConnection GetDbConnection<TContext>(this TContext context)
            where TContext : DbContext
        {
            return context.Database.GetDbConnection();
        }

        public static DbConnection GetDbConnection(this DbContext context)
        {
            return context.Database.GetDbConnection();
        }

        public static TContext Delete<TContext, TModel>(this TContext context, TModel? model)
            where TContext : DbContext
            where TModel : class
        {
            if (model == null) return context;

            _ = context.Set<TModel>().Remove(model);
            return context;
        }

        public static DbContext Delete<TModel>(this DbContext context, TModel? model)
            where TModel : class
        {
            if (model == null) return context;

            _ = context.Set<TModel>().Remove(model);
            return context;
        }

        public static TContext Delete<TContext, TModel, TKey>(this TContext context, string keyName, params TKey[] ids)
            where TContext : DbContext
            where TModel : class, new()
        {
            if (ids.IsNullOrEmpty()) return context;

            var models = new TModel[ids.Length];
            var property = typeof(TModel).GetProperty(keyName);
            if (property == null)
                throw new InvalidOperationException($"当前类型没有[{keyName}]属性");

            for (var i = 0; i < ids.Length; i++)
            {
                var key = ids[i];
                var model = new TModel();
                property.SetValue(model, key);
                models[i] = model;
            }

            context.Set<TModel>().RemoveRange(models);

            return context;
        }

        public static DbContext Delete<TModel, TKey>(this DbContext context, string keyName, params TKey[] ids)
            where TModel : class, new()
        {
            if (ids.IsNullOrEmpty()) return context;

            var models = new TModel[ids.Length];
            var property = typeof(TModel).GetProperty(keyName);
            if (property == null)
                throw new InvalidOperationException(message: $"当前类型没有[{keyName}]属性");

            for (var i = 0; i < ids.Length; i++)
            {
                var key = ids[i];
                var model = new TModel();
                property.SetValue(model, key);
                models[i] = model;
            }

            context.Set<TModel>().RemoveRange(models);

            return context;
        }

        public static TContext DeleteByDefaultKey<TContext, TModel, TKey>(this TContext context, params TKey[] ids)
            where TContext : DbContext
            where TModel : class, new()
        {
            return context.Delete<TContext, TModel, TKey>(keyName: "Id", ids: ids);
        }

        public static DbContext DeleteByDefaultKey<TModel, TKey>(this DbContext context, params TKey[] ids)
            where TModel : class, new()
        {
            return context.Delete<TModel, TKey>(keyName: "Id", ids: ids);
        }

        public static TContext Delete<TContext, TModel>(this TContext context, Expression<Func<TModel, bool>> predicate)
            where TContext : DbContext
            where TModel : class
        {
            var dbSet = context.Set<TModel>();
            dbSet.RemoveRange(dbSet.Where(predicate));

            return context;
        }

        public static DbContext Delete<TModel>(this DbContext context, Expression<Func<TModel, bool>> predicate)
            where TModel : class
        {
            var dbSet = context.Set<TModel>();
            dbSet.RemoveRange(dbSet.Where(predicate));

            return context;
        }

        public static TContext DeleteMany<TContext, TModel>(this TContext context, IEnumerable<TModel?> models)
            where TContext : DbContext
            where TModel : class
        {
            if (models.IsNullOrEmpty()) return context;

            context.Set<TModel>().RemoveRange(models.OfType<TModel>());
            return context;
        }

        public static DbContext DeleteMany<TModel>(this DbContext context, IEnumerable<TModel?> models)
            where TModel : class
        {
            if (models.IsNullOrEmpty()) return context;

            context.Set<TModel>().RemoveRange(models.OfType<TModel>());
            return context;
        }

        public static Task<TModel?> Get<TContext, TModel>(this TContext context, Expression<Func<TModel, bool>> expression, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            return context.Set<TModel>().AsNoTracking().FirstOrDefaultAsync(expression, cancellationToken);
        }

        public static Task<TModel?> Get<TModel>(this DbContext context, Expression<Func<TModel, bool>> expression, CancellationToken cancellationToken = default)
            where TModel : class
        {
            return context.Set<TModel>().AsNoTracking().FirstOrDefaultAsync(expression, cancellationToken);
        }

        public static Task<List<TModel>> GetList<TContext, TModel>(this TContext context, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            return context.Set<TModel>().AsNoTracking().ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> GetList<TModel>(this DbContext context, CancellationToken cancellationToken = default)
            where TModel : class
        {
            return context.Set<TModel>().AsNoTracking().ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> GetList<TContext, TModel>(this TContext context, Expression<Func<TModel, bool>> expression, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            return context.Set<TModel>().AsNoTracking().Where(expression).ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> GetList<TModel>(this DbContext context, Expression<Func<TModel, bool>> expression, CancellationToken cancellationToken = default)
            where TModel : class
        {
            return context.Set<TModel>().AsNoTracking().Where(expression).ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> GetPageList<TContext, TModel>(this TContext context, Expression<Func<TModel, bool>> expression, int skipCount, int maxResultCount, string sorting, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            return context.Set<TModel>().AsNoTracking().Where(expression).OrderBy(sorting).Skip(skipCount).Take(maxResultCount).ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> GetPageList<TModel>(this DbContext context, Expression<Func<TModel, bool>> expression, int skipCount, int maxResultCount, string sorting, CancellationToken cancellationToken = default)
            where TModel : class
        {
            return context.Set<TModel>().AsNoTracking().Where(expression).OrderBy(sorting).Skip(skipCount).Take(maxResultCount).ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> GetPageList<TContext, TModel>(this TContext context, int skipCount, int maxResultCount, string sorting, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            return context.Set<TModel>().AsNoTracking().OrderBy(sorting).Skip(skipCount).Take(maxResultCount).ToListAsync(cancellationToken);
        }

        public static Task<List<TModel>> GetPageList<TModel>(this DbContext context, int skipCount, int maxResultCount, string sorting, CancellationToken cancellationToken = default)
            where TModel : class
        {
            return context.Set<TModel>().AsNoTracking().OrderBy(sorting).Skip(skipCount).Take(maxResultCount).ToListAsync(cancellationToken);
        }

        public static async Task<TContext> Insert<TContext, TModel>(this TContext context, TModel? model, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            if (model == null) return context;
            _ = await context.Set<TModel>().AddAsync(model, cancellationToken);

            return context;
        }

        public static async Task<DbContext> Insert<TModel>(this DbContext context, TModel? model, CancellationToken cancellationToken = default)
            where TModel : class
        {
            if (model == null) return context;

            _ = await context.Set<TModel>().AddAsync(model, cancellationToken);
            return context;
        }

        public static async Task<TContext> InsertMany<TContext, TModel>(this TContext context, IEnumerable<TModel?> models, CancellationToken cancellationToken = default)
            where TContext : DbContext
            where TModel : class
        {
            if (models.IsNullOrEmpty()) return context;

            await context.Set<TModel>().AddRangeAsync(models.OfType<TModel>(), cancellationToken);
            return context;
        }

        public static async Task<DbContext> InsertMany<TModel>(this DbContext context, IEnumerable<TModel?> models, CancellationToken cancellationToken = default)
            where TModel : class
        {
            if (models.IsNullOrEmpty()) return context;

            await context.Set<TModel>().AddRangeAsync(models.OfType<TModel>(), cancellationToken);
            return context;
        }

        public static TContext UpdateEx<TContext, TModel>(this TContext context, TModel? model)
            where TContext : DbContext
            where TModel : class
        {
            if (model == null) return context;

            AttachIfNot(context, model);
            _ = context.Set<TModel>().Update(model);
            return context;
        }

        public static TContext UpdateExMany<TContext, TModel>(this TContext context, IEnumerable<TModel?> models)
            where TContext : DbContext
            where TModel : class
        {
            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models)
            {
                if (model == null) continue;

                AttachIfNot(context, model);
                _ = context.Set<TModel>().Update(model);
            }

            return context;
        }

        public static DbContext UpdateEx<TModel>(this DbContext context, TModel? model)
            where TModel : class
        {
            if (model == null) return context;

            AttachIfNot(context, model);
            _ = context.Set<TModel>().Update(model);
            return context;
        }

        public static DbContext UpdateExMany<TModel>(this DbContext context, IEnumerable<TModel?> models)
            where TModel : class
        {
            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models)
            {
                if (model == null) continue;

                AttachIfNot(context, model);
                _ = context.Set<TModel>().Update(model);
            }

            return context;
        }

        public static TContext UpdatePartial<TContext, TModel>(this TContext context, TModel model, params Expression<Func<TModel, object?>>[] properties)
            where TContext : DbContext
            where TModel : class
        {
            if (model == null || properties.Length == 0) return context;

            var entity = context.Entry(model);
            foreach (var property in properties)
            {
                entity.Property(property).IsModified = true;
            }

            return context;
        }

        public static TContext UpdatePartialMany<TContext, TModel>(this TContext context, IEnumerable<TModel?> models, params Expression<Func<TModel, object?>>[] properties)
            where TContext : DbContext
            where TModel : class
        {
            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models.OfType<TModel>())
            {
                var entity = context.Entry(model);
                foreach (var property in properties)
                {
                    entity.Property(property).IsModified = true;
                }
            }

            return context;
        }

        public static DbContext UpdatePartial<TModel>(this DbContext context, TModel model, params Expression<Func<TModel, object?>>[] properties)
            where TModel : class
        {
            if (model == null || properties.Length == 0) return context;

            var entity = context.Entry(model);
            foreach (var property in properties)
            {
                entity.Property(property).IsModified = true;
            }

            return context;
        }

        public static DbContext UpdatePartialMany<TModel>(this DbContext context, IEnumerable<TModel> models, params Expression<Func<TModel, object?>>[] properties)
            where TModel : class
        {
            if (models.IsNullOrEmpty() || properties.Length == 0) return context;

            foreach (var model in models)
            {
                var entity = context.Entry(model);
                foreach (var property in properties)
                {
                    entity.Property(property).IsModified = true;
                }
            }

            return context;
        }

        public static TContext UpdatePartial<TContext, TModel>(this TContext context, TModel? model, params string[] properties)
            where TContext : DbContext
            where TModel : class
        {
            if (model == null || properties.Length == 0) return context;

            var entity = context.Entry(model);
            foreach (var property in properties)
            {
                entity.Property(property).IsModified = true;
            }

            return context;
        }

        public static TContext UpdatePartialMany<TContext, TModel>(this TContext context, IEnumerable<TModel?> models, params string[] properties)
            where TContext : DbContext
            where TModel : class
        {
            if (models.IsNullOrEmpty() || properties.Length == 0) return context;

            foreach (var model in models.OfType<TModel>())
            {
                var entity = context.Entry(model);
                foreach (var property in properties)
                {
                    entity.Property(property).IsModified = true;
                }
            }

            return context;
        }

        public static DbContext UpdatePartial<TModel>(this DbContext context, TModel? model, params string[] properties)
            where TModel : class
        {
            if (model == null || properties.Length == 0) return context;

            var entity = context.Entry(model);
            foreach (var property in properties)
            {
                entity.Property(property).IsModified = true;
            }
            return context;
        }

        public static DbContext UpdatePartialMany<TModel>(this DbContext context, IEnumerable<TModel?> models, params string[] properties)
            where TModel : class
        {
            if (models.IsNullOrEmpty() || properties.Length == 0) return context;

            foreach (var model in models.OfType<TModel>())
            {
                var entity = context.Entry(model);
                foreach (var property in properties)
                {
                    entity.Property(property).IsModified = true;
                }
            }

            return context;
        }

        public static TContext UpdatePartialByExclude<TContext, TModel>(this TContext context, TModel? model, params string[] excludeProperties)
            where TContext : DbContext
            where TModel : class
        {
            if (model == null) return context;

            var entry = context.Entry(model);
            foreach (var property in model.GetType().GetProperties().Where(x => !excludeProperties.Contains(x.Name)))
            {
                entry.Property(property.Name).IsModified = true;
            }
            return context;
        }

        public static TContext UpdatePartialManyByExclude<TContext, TModel>(this TContext context, IEnumerable<TModel?> models, params string[] excludeProperties)
            where TContext : DbContext
            where TModel : class
        {
            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models)
            {
                if (model == null) continue;

                var entry = context.Entry(model);
                foreach (var property in model.GetType().GetProperties().Where(x => !excludeProperties.Contains(x.Name)))
                {
                    entry.Property(property.Name).IsModified = true;
                }
            }

            return context;
        }

        public static DbContext UpdatePartialByExclude<TModel>(this DbContext context, TModel? model, params string[] excludeProperties)
            where TModel : class
        {
            if (model == null) return context;

            var entry = context.Entry(model);
            foreach (var property in model.GetType().GetProperties().Where(x => !excludeProperties.Contains(x.Name)))
            {
                entry.Property(property.Name).IsModified = true;
            }
            return context;
        }

        public static DbContext UpdatePartialManyByExclude<TModel>(this DbContext context, IEnumerable<TModel?> models, params string[] excludeProperties)
            where TModel : class
        {
            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models)
            {
                if (model == null) continue;

                var entry = context.Entry(model);
                foreach (var property in model.GetType().GetProperties().Where(x => !excludeProperties.Contains(x.Name)))
                {
                    entry.Property(property.Name).IsModified = true;
                }
            }

            return context;
        }

        public static TContext UpdateMany<TContext, TModel>(this TContext context, IEnumerable<TModel?> models)
            where TContext : DbContext
            where TModel : class
        {
            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models.OfType<TModel>())
            {
                AttachIfNot(context, model);
            }
            context.Set<TModel>().UpdateRange(models.OfType<TModel>());

            return context;
        }

        public static DbContext UpdateMany<TModel>(this DbContext context, IEnumerable<TModel?> models)
            where TModel : class
        {
            if (models.IsNullOrEmpty()) return context;

            foreach (var model in models.OfType<TModel>())
            {
                AttachIfNot(context, model);
            }
            context.Set<TModel>().UpdateRange(models.OfType<TModel>());

            return context;
        }

        private static void AttachIfNot<TContext, TModel>(TContext context, TModel? model)
            where TContext : DbContext
            where TModel : class
        {
            if (model == null) return;

            var entry = context.ChangeTracker.Entries().FirstOrDefault((ent) => ent.Entity == model);
            if (entry == null)
            {
                context.Attach(model);
            }
        }

        public static Task<Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction> BeginTransaction<TContext>(this TContext context)
            where TContext : DbContext
        {
            return context.Database.BeginTransactionAsync();
        }

        public static bool IsNullOrEmpty<T>(this IEnumerable<T> enumerable)
        {
            if (enumerable != null)
            {
                return !enumerable.Any();
            }

            return true;
        }
    }
}